# coding:utf-8
import numpy as np
import pandas as pd
import pymysql
import sys, random

"""
推荐算法
1. 配置推荐个数参数、数据库参数
2. 通过 js传递参数 python Recommend.py userId taste food(通过逗号,分割)
3. 获取当前用户已经摄入的营养值
4. 计算当前用户还可摄入的营养值
5. 通过数据库 FOOD查找当前口味、当前营养值范围，用户可以摄入的食物，作为预测食物
6. 通过数据库 USERFOOD选择出用户已经选择过、且不在预测食物列表中的食物，作为对照食物
7. 如果对照食物小于 2倍的 cnt，则采用随机推荐 (8)，否则采用协同过滤 (9-12)
8. 针对每一种预测食物，随机生成预测点击次数，降序排序，不足 cnt个则全部返回，否则返回前 cnt个
9. 针对每一种预测食物和对照食物，寻找同时选择过当前预测食物和当对照食物的用户，记录两种食物的点击次数，生成两个向量
10. 利用皮尔逊系数计算两个向量即两种食物的相似程度，并作为权值乘以当前用户对对照食物的点击次数，作为这个预测食物单次的预测结果
11. 针对每一种预测食物，求出其对每个对照食物的单次预测结果，加权平均，作为这个预测食物的预测点击次数
12. 求出所有预测食物的预测点击次数，降序排序，不足 cnt个则全部返回，否则返回前 cnt个
"""

# 参数配置
"""
1. 推荐个数参数
2. 数据库参数
"""
cnt = 30
HOST = 'localhost'
USER = 'root'
PASSWORD = '123456'
PORT = 3306
DATABASE = 'jisuanjishejidasai'

# 获取待预测的食物id列表
"""
1. 从数据库 NUTRITION获取用户已吃食物的营养参数
2. 根据性别计算用户还能吃的营养值上限
3. 根据用户的口味模糊匹配数据库 FOOD，并排除营养值超上限的食物
4. 返回待推荐的健康食物id列表
"""
def GetPredictFood(userId, foodList, taste):
    # 获取用户性别
    db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, port=PORT, db=DATABASE, charset='utf8')
    cur = db.cursor()
    cur.execute('SELECT * FROM USERS WHERE NAME = "' + userId + '";');
    sex = cur.fetchone()[3]
    db.close()

    # 获取各食物营养参数  
    protein = 0.0
    fat = 0.0
    carb = 0.0
    heat = 0.0
    db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, port=PORT, db=DATABASE, charset='utf8')
    cur = db.cursor()
    for food in foodList:
        cur.execute('SELECT * FROM NUTRITION WHERE NAME = "%s";' % food)
        res = cur.fetchone()
        protein += res[2]
        fat += res[3]
        carb += res[4]
        heat += res[5]
    db.close()
    
    # 计算还可摄入的营养参数值
    """
                蛋白质：
                            男子：75g/day
                            女子：65g/day
                脂肪：
            50g/day
                碳水化合物：
                            总摄入量的55%~65%，即：
                                男子：x/(75+50+x)=0.6 ==> 187.5g/day
                                女子：x/(65+50+x)=0.6 ==> 172.5g/day
                热量：
                            男子：2400千卡/day
                            女子：2100千卡/day
    """
    canProtein = 0.0
    canFat = 0.0
    canCarb = 0.0
    canHeat = 0.0
    if sex is 0:
        canProtein = 75 - protein
        canFat = 50 - fat
        canCarb = 187.5 - carb
        canHeat = 2400 - heat
    if sex is 1:
        canProtein = 65 - protein
        canFat = 50 - fat
        canCarb = 172.5 - carb
        canHeat = 2100 - heat
        
    # 获取当前营养值可以享用的食物
    db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, port=PORT, db=DATABASE, charset='utf8')
    cur = db.cursor()  
    cur.execute('SELECT * FROM FOOD WHERE TYPE LIKE "%' + taste + '%";')
    results = list(cur.fetchall())
    i = 0
    while i < len(results):
        needList = results[i][2].split('、')
        tempProtein = 0.0
        tempFat = 0.0
        tempCarb = 0.0
        tempHeat = 0.0
        for need in needList:
            cur.execute('SELECT * FROM NUTRITION WHERE NAME = "%s";' % (need))
            nut = cur.fetchone()
            if nut != None:
                tempProtein += nut[2]
                tempFat += nut[3]
                tempCarb += nut[4]
                tempHeat += nut[5]
        if tempProtein > canProtein or tempFat > canFat or tempCarb > canCarb or tempHeat > canHeat:
            results.remove(results[i])
            i -= 1
        i += 1
    db.close()
    
    return [e[0] for e in results]

# 获取对照的食物id列表
"""
1. 根据 userId获取数据库 USERFOOD中该用户已经选择过的所有食物
2. 因对预测食物列表与对照食物列表不同，排除其中含有的待预测的食物
3. 返回对照的食物id列表
"""
def GetComparedFood(userId, predictFoodIdList, taste):
    # 计算数据库 USERFOOD中是否有多少数据不在predictFoodList，以此作为对照数据
    db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, port=PORT, db=DATABASE, charset='utf8')
    cur = db.cursor()
    cur.execute('SELECT USERFOOD.* FROM USERFOOD JOIN FOOD \
                    WHERE FOOD.ID = USERFOOD.FOODID AND \
                    USERFOOD.USERID = "' + userId + '" AND \
                    FOOD.TYPE LIKE "%' + taste + '%";')
    userfoodList = list(cur.fetchall())  # USERFOOD数据库中当前user所有的食物
    db.close()
    i = 0
    while i < len(userfoodList):
        if userfoodList[i][2] in predictFoodIdList:
            userfoodList.remove(userfoodList[i])
            i -= 1
        i += 1
    comparedFoodList = userfoodList[:]  # 对照数据
    return [e[2] for e in comparedFoodList]

# 如果对照食物数量不足cnt个，则随机推荐
"""
1. 为每一条预测食物生成一个随机数作为推荐值，并排序
2. 如果推荐食物的数量小于cnt个，则全部返回
3. 如果推荐食物的数量大于等于cnt个，则返回前cnt个
"""
def RandomRecommend(predictFoodIdList, cnt):
    recommendFoodIdList = []
    for predictFoodId in predictFoodIdList:
        recommendFoodIdList.append([predictFoodId, random.random() * 100])
    recommendFoodIdList.sort(key=lambda x:x[1], reverse=True)
    recommendFoodIdList = [e[0] for e in recommendFoodIdList]
    if len(recommendFoodIdList) < cnt:
        return recommendFoodIdList
    else:
        return recommendFoodIdList[:cnt] 
    
# 皮尔逊系数
def pears(colA, colB):
    if len(colA) < 3:
        return 1.0
    return 0.5 + 0.5 * np.corrcoef(colA, colB, rowvar=0)[0][1]

# 获取预测食物的点击次数与对照食物的点击次数的两个列向量
def GetPredictFoodValueListAndComparedFoodValueList(fullData, predictFoodId, comparedFoodId):
    PredictFoodValueList = []
    ComparedFoodValueList = []
    # 获取所有同样选择过这个预测食物的数据
    userfoodList = fullData[fullData['foodid'] == predictFoodId]
    for i, food in userfoodList.iterrows():
        # 针对每一个选过预测食物的人，获取他们的id
        comparedUserId = food['userid']
        # 获取这个人的所有食物
        comparedUserFoodList = fullData[fullData['userid'] == comparedUserId]
        number = []
        for i, f in comparedUserFoodList.iterrows():
            # 查找这个人是否选择过对照食物,如果是则加入number
            if f['foodid'] == comparedFoodId:
                number.append(f['number'])
        if len(number) == 0:
            continue
        else:
            # 最终获取所有选择预测食物和对照食物的人对预测食物的计数
            PredictFoodValueList.append([food['number']])
            # 最终获取所有选择预测食物和对照食物的人对对照食物的计数
            ComparedFoodValueList.append(number)
    return np.mat(PredictFoodValueList), np.mat(ComparedFoodValueList)

# 如果对照食物数量大于等于cnt个，则采用协同过滤算法
"""
1. 获取数据库 USERFOOD中当前口味的所有数据
2. 遍历预测食物id列表，遍历对照食物id列表，寻找同时选择过两种食物的用户，创建预测食物点击次数和对照食物点击次数的列向量
3. 利用皮尔逊系数判断两个列向量的相似程度，作为权值，乘以用户对对照食物的点击次数，作为单次预测结果
4. 针对同一个预测食物，计算出不同对照食物的单次预测结果，加权预测用户对预测食物的点击次数
5. 对推荐食物按照预测点击次数进行排序，小于cnt个则全部返回，大于等于cnt个则返回前cnt个
"""
def CollaborativeFilteringRecommend(userId, predictFoodIdList, comparedFoodIdList, taste, cnt):
    # 获取数据库 USERFOOD中除了当前用户以外的所有当前口味的数据
    db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, port=PORT, db=DATABASE, charset='utf8')
    fullData = pd.read_sql('SELECT USERFOOD.* FROM USERFOOD JOIN FOOD WHERE \
                    USERFOOD.FOODID = FOOD.ID AND \
                    FOOD.TYPE LIKE "%' + taste + '%";', con=db)
    fullData.drop('id', axis=1, inplace=True)
    db.close()
    
    # 进行食物预测
    recommendFoodDataFrame = pd.DataFrame(columns=['foodid', 'number'])
    predictFood = []
    predictNumber = []
    # 预测食物一重循环
    for predictFoodId in predictFoodIdList:
        # 累加的相似度
        simTotal = 0.0
        # 累加的相似度乘以当前食物的计数
        ratSimTotal = 0.0
        # 对照食物二重循环
        for comparedFoodId in comparedFoodIdList:
            # 获取向量值
            PredictFoodValueList, ComparedFoodValueList = \
                 GetPredictFoodValueListAndComparedFoodValueList(fullData, predictFoodId, comparedFoodId)
            # 获取当前用户对对照食物的点击次数
            number = fullData[fullData['foodid'] == comparedFoodId]
            number = number[number['userid'] == int(userId)]['number']
            # 计算相似程度
            similarity = pears(PredictFoodValueList, ComparedFoodValueList)
            simTotal += similarity
            ratSimTotal += similarity * number.values[0]
        predictFood.append(predictFoodId)
        predictNumber.append(ratSimTotal / simTotal)
        
    recommendFoodDataFrame['foodid'] = predictFood
    recommendFoodDataFrame['number'] = predictNumber
    recommendFoodDataFrame = recommendFoodDataFrame.sort_values(by='number', ascending=False)
    
    recommendFoodIdList = recommendFoodDataFrame['foodid'].tolist()
    if len(recommendFoodIdList) < cnt:
        return recommendFoodIdList
    else:
        return recommendFoodIdList[:cnt]

# 推荐算法
"""
1. 对照食物数量应达到 2倍的推荐食物数量
2. 如果对照食物数量小于 2倍的 cnt，则随机推荐
3. 如果对照食物数量大于 2倍的 cnt，则协同过滤推荐
"""   
def GetRecommendFood(userId, predictFoodIdList, comparedFoodIdList, taste, cnt):
    if len(comparedFoodIdList) < 2 * cnt:
        recommendFoodIdList = RandomRecommend(predictFoodIdList, cnt)
    else:
        recommendFoodIdList = CollaborativeFilteringRecommend(userId, predictFoodIdList, comparedFoodIdList, taste, cnt)
    return recommendFoodIdList

"""
-----主函数开始 -----
"""
# 获取参数
userId = sys.argv[1]
taste = sys.argv[2]
foodList = sys.argv[3].split(',')

# 获取待预测的食物列表
predictFoodIdList = GetPredictFood(userId, foodList, taste)
# 获取对照的食物列表
comparedFoodIdList = GetComparedFood(userId, predictFoodIdList, taste)
# 对照食物列表小于cnt个，随机推荐，大于等于cnt个，协同过滤推荐
recommendFoodIdList = GetRecommendFood(userId, predictFoodIdList, comparedFoodIdList, taste, cnt)
# 返回
print(recommendFoodIdList)

"""
-----主函数结束-----
"""

